/**
 * @author HypnosNova / https://www.threejs.org.cn/gallery
 * This is a class to check whether objects are in a selection area in 3D space
 */

import { Frustum, Vector3, MeshLambertMaterial, MeshBasicMaterial, BackSide, Mesh, FrontSide } from 'three'
import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js'

var SelectionBox = (function () {
  var frustum = new Frustum()
  var center = new Vector3()

  var tmpPoint = new Vector3()

  var vecNear = new Vector3()
  var vecTopLeft = new Vector3()
  var vecTopRight = new Vector3()
  var vecDownRight = new Vector3()
  var vecDownLeft = new Vector3()

  var vecFarTopLeft = new Vector3()
  var vecFarTopRight = new Vector3()
  var vecFarDownRight = new Vector3()
  var vecFarDownLeft = new Vector3()

  var vectemp1 = new Vector3()
  var vectemp2 = new Vector3()
  var vectemp3 = new Vector3()

  function SelectionBox(camera, scene, deep) {
    this.camera = camera
    this.scene = scene
    this.startPoint = new Vector3()
    this.endPoint = new Vector3()
    this.selection = null
    this.deep = deep || Number.MAX_VALUE
  }

  SelectionBox.prototype.select = function (startPoint, endPoint) {
    this.startPoint = startPoint || this.startPoint
    this.endPoint = endPoint || this.endPoint
    this.selection = null

    this.updateFrustum(this.startPoint, this.endPoint)
    this.searchChildInFrustum(frustum, this.scene)

    return this.selection
  }

  SelectionBox.prototype.updateFrustum = function (startPoint, endPoint) {
    startPoint = startPoint || this.startPoint
    endPoint = endPoint || this.endPoint

    this.camera.updateProjectionMatrix()
    this.camera.updateMatrixWorld()

    if (this.camera.isPerspectiveCamera) {
      tmpPoint.copy(startPoint)
      tmpPoint.x = Math.min(startPoint.x, endPoint.x)
      tmpPoint.y = Math.max(startPoint.y, endPoint.y)
      endPoint.x = Math.max(startPoint.x, endPoint.x)
      endPoint.y = Math.min(startPoint.y, endPoint.y)

      vecNear.copy(this.camera.position)
      vecTopLeft.copy(tmpPoint)
      vecTopRight.set(endPoint.x, tmpPoint.y, 0)
      vecDownRight.copy(endPoint)
      vecDownLeft.set(tmpPoint.x, endPoint.y, 0)

      vecTopLeft.unproject(this.camera)
      vecTopRight.unproject(this.camera)
      vecDownRight.unproject(this.camera)
      vecDownLeft.unproject(this.camera)

      vectemp1.copy(vecTopLeft).sub(vecNear)
      vectemp2.copy(vecTopRight).sub(vecNear)
      vectemp3.copy(vecDownRight).sub(vecNear)
      vectemp1.normalize()
      vectemp2.normalize()
      vectemp3.normalize()

      vectemp1.multiplyScalar(this.deep)
      vectemp2.multiplyScalar(this.deep)
      vectemp3.multiplyScalar(this.deep)
      vectemp1.add(vecNear)
      vectemp2.add(vecNear)
      vectemp3.add(vecNear)

      var planes = frustum.planes

      planes[0].setFromCoplanarPoints(vecNear, vecTopLeft, vecTopRight)
      planes[1].setFromCoplanarPoints(vecNear, vecTopRight, vecDownRight)
      planes[2].setFromCoplanarPoints(vecDownRight, vecDownLeft, vecNear)
      planes[3].setFromCoplanarPoints(vecDownLeft, vecTopLeft, vecNear)
      planes[4].setFromCoplanarPoints(vecTopRight, vecDownRight, vecDownLeft)
      planes[5].setFromCoplanarPoints(vectemp3, vectemp2, vectemp1)
      planes[5].normal.multiplyScalar(-1)
    } else if (this.camera.isOrthographicCamera) {
      if (startPoint.equals(endPoint)) endPoint.addScalar(Number.EPSILON) // avoid invalid frustum

      var left = Math.min(startPoint.x, endPoint.x)
      var top = Math.max(startPoint.y, endPoint.y)
      var right = Math.max(startPoint.x, endPoint.x)
      var down = Math.min(startPoint.y, endPoint.y)

      vecTopLeft.set(left, top, -1)
      vecTopRight.set(right, top, -1)
      vecDownRight.set(right, down, -1)
      vecDownLeft.set(left, down, -1)

      vecFarTopLeft.set(left, top, 1)
      vecFarTopRight.set(right, top, 1)
      vecFarDownRight.set(right, down, 1)
      vecFarDownLeft.set(left, down, 1)

      vecTopLeft.unproject(this.camera)
      vecTopRight.unproject(this.camera)
      vecDownRight.unproject(this.camera)
      vecDownLeft.unproject(this.camera)

      vecFarTopLeft.unproject(this.camera)
      vecFarTopRight.unproject(this.camera)
      vecFarDownRight.unproject(this.camera)
      vecFarDownLeft.unproject(this.camera)

      var planes = frustum.planes

      planes[0].setFromCoplanarPoints(vecTopLeft, vecFarTopLeft, vecFarTopRight)
      planes[1].setFromCoplanarPoints(vecTopRight, vecFarTopRight, vecFarDownRight)
      planes[2].setFromCoplanarPoints(vecFarDownRight, vecFarDownLeft, vecDownLeft)
      planes[3].setFromCoplanarPoints(vecFarDownLeft, vecFarTopLeft, vecTopLeft)
      planes[4].setFromCoplanarPoints(vecTopRight, vecDownRight, vecDownLeft)
      planes[5].setFromCoplanarPoints(vecFarDownRight, vecFarTopRight, vecFarTopLeft)
      planes[5].normal.multiplyScalar(-1)
    } else {
      console.error('THREE.SelectionBox: Unsupported camera type.')
    }
  }

  SelectionBox.prototype.searchChildInFrustum = function (frustum, object) {
    if (object.isPoints && !object.isBackground) {
      const pointsIndex = []
      const minusPointsIndex = []
      const vertices = []
      let zRange = 0
      const pointsArray = object.geometry.attributes.position ? object.geometry.attributes.position.array : []
      for (let i = 0; i < pointsArray.length; i += 3) {
        center.set(pointsArray[i], pointsArray[i + 1], pointsArray[i + 2])

        if (frustum.containsPoint(center)) {
          let intensity = 1
          if (object.geometry.attributes.intensities) intensity = object.geometry.attributes.intensities.array[i / 3]
          if (this.labelTool === 'minus') {
            if (object.geometry.attributes.originalColor) {
              // object.geometry.attributes.color.array[i] = object.geometry.attributes.originalColor.array[i];
              // object.geometry.attributes.color.array[i + 1] = object.geometry.attributes.originalColor.array[i + 1];
              // object.geometry.attributes.color.array[i + 2] = object.geometry.attributes.originalColor.array[i + 2];

              object.geometry.attributes.color.array[i] = 0
              object.geometry.attributes.color.array[i + 1] = 0
              object.geometry.attributes.color.array[i + 2] = 1 /* intensity*/
              minusPointsIndex.push(i / 3)
            }
          } else if (this.labelTool === 'point') {
            if (object.geometry.attributes.originalColor) {
              const vertex = new Vector3(pointsArray[i], pointsArray[i + 1], pointsArray[i + 2])
              vertices.push(vertex)
              zRange += Math.abs(pointsArray[i + 2])

              object.geometry.attributes.color.array[i] = 1 /* intensity*/
              object.geometry.attributes.color.array[i + 1] = 0
              object.geometry.attributes.color.array[i + 2] = 0
              pointsIndex.push(i / 3)
            }
          }
        }
      }

      if (zRange == 0 && vertices.length > 0) {
        const v = vertices[vertices.length - 1]
        const vertex = new Vector3(v.x, v.y, v.z + 0.01)
        vertices.push(vertex)
      }

      let mesh = null
      if (vertices.length > 4) {
        const meshMaterial = new MeshBasicMaterial({
          color: 'rgb(0, 255, 0)',
          opacity: 0.1,
          transparent: true,
        })

        const meshGeometry = new ConvexGeometry(vertices)

        mesh = new Mesh(meshGeometry, meshMaterial.clone())
        mesh.material.side = FrontSide
        mesh.renderOrder = 1
      }

      object.geometry.attributes.color.needsUpdate = true

      if (pointsIndex.length > 0 || minusPointsIndex.length > 0) {
        const selection = {
          mesh,
          pointsIndex,
          minusPointsIndex,
        }

        this.selection = selection
      }

      return
    }

    if (object.children.length > 0) {
      for (var x = 0; x < object.children.length; x++) {
        this.searchChildInFrustum(frustum, object.children[x])
      }
    }
  }

  return SelectionBox
})()

export { SelectionBox }
